Java 26일 코스 - Day 21: JDBC 데이터베이스

Day 21: JDBC 데이터베이스

JDBC(Java Database Connectivity)는 Java에서 데이터베이스에 접속하고 SQL을 실행하는 표준 API입니다. 다양한 데이터베이스(MySQL, PostgreSQL, Oracle, H2 등)를 동일한 인터페이스로 다룰 수 있습니다. 여기서는 가볍고 설정이 필요 없는 H2 인메모리 데이터베이스로 실습합니다.

데이터베이스 연결과 테이블 생성

JDBC로 데이터베이스에 연결하고 테이블을 만듭니다.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcConnection {
    // H2 인메모리 DB URL
    private static final String DB_URL = "jdbc:h2:mem:testdb";
    private static final String DB_USER = "sa";
    private static final String DB_PASSWORD = "";

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
    }

    public static void main(String[] args) {
        // try-with-resources로 자동 리소스 해제
        try (Connection conn = getConnection();
             Statement stmt = conn.createStatement()) {

            System.out.println("DB 연결 성공!");
            System.out.println("DB 제품: " + conn.getMetaData().getDatabaseProductName());

            // 테이블 생성
            String createTable = """
                CREATE TABLE users (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    name VARCHAR(50) NOT NULL,
                    email VARCHAR(100) UNIQUE,
                    age INT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
                """;
            stmt.execute(createTable);
            System.out.println("users 테이블 생성 완료");

        } catch (SQLException e) {
            System.err.println("DB 에러: " + e.getMessage());
        }
    }
}

CRUD 작업 (PreparedStatement)

SQL 인젝션을 방지하는 PreparedStatement로 데이터를 다룹니다.

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

record User(int id, String name, String email, int age) {}

public class JdbcCrud {
    private static final String URL = "jdbc:h2:mem:testdb";

    static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, "sa", "");
    }

    // CREATE
    static int insertUser(Connection conn, String name, String email, int age)
            throws SQLException {
        String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
        try (PreparedStatement pstmt = conn.prepareStatement(sql,
                Statement.RETURN_GENERATED_KEYS)) {
            pstmt.setString(1, name);
            pstmt.setString(2, email);
            pstmt.setInt(3, age);
            pstmt.executeUpdate();

            try (ResultSet keys = pstmt.getGeneratedKeys()) {
                if (keys.next()) return keys.getInt(1);
            }
        }
        return -1;
    }

    // READ (전체)
    static List<User> findAllUsers(Connection conn) throws SQLException {
        String sql = "SELECT id, name, email, age FROM users ORDER BY id";
        List<User> users = new ArrayList<>();
        try (Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            while (rs.next()) {
                users.add(new User(
                    rs.getInt("id"),
                    rs.getString("name"),
                    rs.getString("email"),
                    rs.getInt("age")
                ));
            }
        }
        return users;
    }

    // READ (조건)
    static User findById(Connection conn, int id) throws SQLException {
        String sql = "SELECT id, name, email, age FROM users WHERE id = ?";
        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, id);
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return new User(rs.getInt("id"), rs.getString("name"),
                                    rs.getString("email"), rs.getInt("age"));
                }
            }
        }
        return null;
    }

    // UPDATE
    static boolean updateUser(Connection conn, int id, String name, int age)
            throws SQLException {
        String sql = "UPDATE users SET name = ?, age = ? WHERE id = ?";
        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, name);
            pstmt.setInt(2, age);
            pstmt.setInt(3, id);
            return pstmt.executeUpdate() > 0;
        }
    }

    // DELETE
    static boolean deleteUser(Connection conn, int id) throws SQLException {
        String sql = "DELETE FROM users WHERE id = ?";
        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, id);
            return pstmt.executeUpdate() > 0;
        }
    }

    public static void main(String[] args) throws SQLException {
        try (Connection conn = getConnection()) {
            // 테이블 생성
            conn.createStatement().execute("""
                CREATE TABLE users (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    name VARCHAR(50), email VARCHAR(100), age INT)
                """);

            // INSERT
            insertUser(conn, "홍길동", "hong@test.com", 25);
            insertUser(conn, "김영희", "kim@test.com", 30);
            insertUser(conn, "이철수", "lee@test.com", 28);

            // SELECT ALL
            System.out.println("=== 전체 목록 ===");
            findAllUsers(conn).forEach(System.out::println);

            // UPDATE
            updateUser(conn, 1, "홍길동(수정)", 26);

            // SELECT ONE
            System.out.println("\n수정 후: " + findById(conn, 1));

            // DELETE
            deleteUser(conn, 2);
            System.out.println("\n=== 삭제 후 ===");
            findAllUsers(conn).forEach(System.out::println);
        }
    }
}

트랜잭션 처리

여러 SQL 작업을 하나의 원자적 단위로 처리합니다.

import java.sql.*;

public class TransactionExample {
    private static final String URL = "jdbc:h2:mem:bankdb";

    static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, "sa", "");
    }

    static void transfer(Connection conn, int fromId, int toId, long amount)
            throws SQLException {
        // 트랜잭션 시작
        conn.setAutoCommit(false);

        try {
            // 출금
            String withdrawSql = "UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?";
            try (PreparedStatement pstmt = conn.prepareStatement(withdrawSql)) {
                pstmt.setLong(1, amount);
                pstmt.setInt(2, fromId);
                pstmt.setLong(3, amount);
                int rows = pstmt.executeUpdate();
                if (rows == 0) {
                    throw new SQLException("출금 실패: 잔액 부족 또는 계좌 없음");
                }
            }

            // 입금
            String depositSql = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
            try (PreparedStatement pstmt = conn.prepareStatement(depositSql)) {
                pstmt.setLong(1, amount);
                pstmt.setInt(2, toId);
                int rows = pstmt.executeUpdate();
                if (rows == 0) {
                    throw new SQLException("입금 실패: 계좌 없음");
                }
            }

            // 이체 기록
            String logSql = "INSERT INTO transfer_log (from_id, to_id, amount) VALUES (?, ?, ?)";
            try (PreparedStatement pstmt = conn.prepareStatement(logSql)) {
                pstmt.setInt(1, fromId);
                pstmt.setInt(2, toId);
                pstmt.setLong(3, amount);
                pstmt.executeUpdate();
            }

            conn.commit(); // 모두 성공하면 커밋
            System.out.println("이체 성공: " + fromId + " -> " + toId + " (" + amount + "원)");

        } catch (SQLException e) {
            conn.rollback(); // 하나라도 실패하면 롤백
            System.err.println("이체 실패 (롤백): " + e.getMessage());
        } finally {
            conn.setAutoCommit(true);
        }
    }

    public static void main(String[] args) throws SQLException {
        try (Connection conn = getConnection();
             Statement stmt = conn.createStatement()) {

            stmt.execute("""
                CREATE TABLE accounts (
                    id INT PRIMARY KEY, name VARCHAR(50), balance BIGINT)
                """);
            stmt.execute("""
                CREATE TABLE transfer_log (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    from_id INT, to_id INT, amount BIGINT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)
                """);
            stmt.execute("INSERT INTO accounts VALUES (1, '홍길동', 100000)");
            stmt.execute("INSERT INTO accounts VALUES (2, '김영희', 50000)");

            System.out.println("=== 이체 전 ===");
            printAccounts(conn);

            transfer(conn, 1, 2, 30000);

            System.out.println("\n=== 이체 후 ===");
            printAccounts(conn);

            // 잔액 부족 시도
            transfer(conn, 1, 2, 999999);
        }
    }

    static void printAccounts(Connection conn) throws SQLException {
        try (ResultSet rs = conn.createStatement()
                .executeQuery("SELECT * FROM accounts")) {
            while (rs.next()) {
                System.out.printf("%s (ID:%d): %,d원%n",
                    rs.getString("name"), rs.getInt("id"), rs.getLong("balance"));
            }
        }
    }
}

DAO 패턴

데이터 접근 로직을 별도 클래스로 분리하는 패턴입니다.

import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

record Product(int id, String name, int price, int stock) {}

// DAO 인터페이스
interface ProductDao {
    int save(Product product);
    Optional<Product> findById(int id);
    List<Product> findAll();
    boolean update(Product product);
    boolean delete(int id);
}

// JDBC 구현
class JdbcProductDao implements ProductDao {
    private final Connection conn;

    JdbcProductDao(Connection conn) {
        this.conn = conn;
    }

    @Override
    public int save(Product product) {
        String sql = "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)";
        try (PreparedStatement ps = conn.prepareStatement(sql,
                Statement.RETURN_GENERATED_KEYS)) {
            ps.setString(1, product.name());
            ps.setInt(2, product.price());
            ps.setInt(3, product.stock());
            ps.executeUpdate();
            try (ResultSet keys = ps.getGeneratedKeys()) {
                return keys.next() ? keys.getInt(1) : -1;
            }
        } catch (SQLException e) {
            throw new RuntimeException("저장 실패", e);
        }
    }

    @Override
    public Optional<Product> findById(int id) {
        String sql = "SELECT * FROM products WHERE id = ?";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setInt(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) return Optional.of(mapRow(rs));
            }
        } catch (SQLException e) {
            throw new RuntimeException("조회 실패", e);
        }
        return Optional.empty();
    }

    @Override
    public List<Product> findAll() {
        List<Product> list = new ArrayList<>();
        try (ResultSet rs = conn.createStatement()
                .executeQuery("SELECT * FROM products")) {
            while (rs.next()) list.add(mapRow(rs));
        } catch (SQLException e) {
            throw new RuntimeException("전체 조회 실패", e);
        }
        return list;
    }

    @Override
    public boolean update(Product p) {
        String sql = "UPDATE products SET name=?, price=?, stock=? WHERE id=?";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, p.name());
            ps.setInt(2, p.price());
            ps.setInt(3, p.stock());
            ps.setInt(4, p.id());
            return ps.executeUpdate() > 0;
        } catch (SQLException e) {
            throw new RuntimeException("수정 실패", e);
        }
    }

    @Override
    public boolean delete(int id) {
        try (PreparedStatement ps = conn.prepareStatement(
                "DELETE FROM products WHERE id=?")) {
            ps.setInt(1, id);
            return ps.executeUpdate() > 0;
        } catch (SQLException e) {
            throw new RuntimeException("삭제 실패", e);
        }
    }

    private Product mapRow(ResultSet rs) throws SQLException {
        return new Product(
            rs.getInt("id"), rs.getString("name"),
            rs.getInt("price"), rs.getInt("stock"));
    }
}

오늘의 연습문제

  1. 게시판 DB: posts 테이블(id, title, content, author, created_at)을 만들고 CRUD 메서드를 구현하세요. 제목으로 검색하는 findByTitle(String keyword) 메서드도 추가하세요.

  2. 배치 삽입: PreparedStatementaddBatch()executeBatch()를 사용하여 1000개의 레코드를 한 번에 삽입하는 프로그램을 작성하세요. 건별 삽입과 배치 삽입의 성능을 비교하세요.

  3. 계좌 이체 시스템: 3개의 계좌 간 이체를 트랜잭션으로 처리하는 프로그램을 작성하세요. A->B 이체 중 에러가 발생하면 롤백되는 것을 확인하고, 이체 이력을 별도 테이블에 기록하세요.

이 글이 도움이 되었나요?